package com.ssm.user.websocket;

import com.ssm.websocket.config.WebSocketServerConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@Slf4j
@ServerEndpoint(value = "/ssm/socket", configurator = WebSocketServerConfig.class)
//它将这个类声明为一个WebSocket端点，指定了连接的URL路径为 ws://localhost:8080 + "/ssm/socket"
// configurator提供配置 WebSocket 端点的自定义配置器
public class ssmWebSocket {

    /**
     * 注意：标注 @ServerEndpoint 注解的类对象是多例的，即每个连接都会创建一个新的对象
     *
     * @OnOpen：当 WebSocket 连接建立时，会调用标注有 @onOpen 的方法
     *
     * @OnClose：当 WebSocket 连接关闭时，会调用标注有 @OnClose 的方法
     *
     * @OnMessage：当收到客户端发送的消息时，会调用标注有 @OnMessage 的方法
     *
     * @OnError：当出现错误时，会调用标注有 @OnError 的方法
     *
     */

    /**
     * 记录当前在线的连接数
     * 提供原子操作的Integer类，通过线程安全的方式操作加减。防止同时上线时,只加1。
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    /**
     * 存放所有的在线客户端
     * HashMap线程不安全，ConcurrentHashMap线程安全
     */
    private static Map<String, ssmWebSocket> clients = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话，需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 当前会话的唯一标识key
     */
    private String erp = "";

    /**
     * 连接建立成功调用的方法
     * @param session
     * @param config  EndpointConfig类可获取前端传递的参数信息
     * @throws IOException
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) throws IOException {
      try {
          Map<String, Object> userProperties = config.getUserProperties(); //获取WebSocket的所有请求参数
          String erp = (String) userProperties.get("erp"); //获取前端传递的erp参数
          this.erp = erp;
          this.session = session;
          if(clients.containsKey(this.erp)) { //当前erp已建立连接,将旧连接关闭
              clients.get(this.erp).session.close();
              clients.remove(this.erp); //关闭连接后，再从集合中删除
              onlineCount.decrementAndGet(); //在线人数减一
          }

          clients.put(this.erp, this); //将新连接加入集合
          onlineCount.incrementAndGet(); //在线人数加一
          log.info("有新连接加入:{},当前在线人数:{}", erp, onlineCount.get());
          sendMessage("连接成功", session); //告诉前端连接成功了
      } catch (Exception e) {
          log.error("建立链接错误{}", e.getMessage(), e);
      }
    }

    /**
     *
     * @param message 给前端发送什么消息
     * @param session 往哪个连接中发消息
     */
    public void sendMessage(String message, Session session) throws IOException {
        log.info("服务端给客户端[{}]发送消息:{}", this.erp, message);
        session.getBasicRemote().sendText(message);
    }

    /**
     * 当WebSocket连接关闭时，调用的方法
     */
    @OnClose
    public void onClose() throws IOException {
       try {
           if(clients.containsKey(this.erp)) { //如果当前erp还未关闭连接，就手动关闭
               clients.get(this.erp).session.close();
               clients.remove(this.erp);
               onlineCount.decrementAndGet();
           }
           log.info("有一处连接关闭{},当前在线人数:{}", this.erp, onlineCount.get());
       } catch (Exception e) {
           log.error("建立链接错误{}", e.getMessage(), e);
       }
    }

    /**
     *当出现错误时，会调用的方法
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        log.error("websocket:{},发送错误,错误原因:{}", erp, throwable.getMessage(), throwable);
        try {
            session.close();
        } catch (Exception e) {
            log.error("onError.Exception{}", e.getMessage(), e);
        }
    }

    /**
     *群发消息，为每一个客户端发送消息
     */
    public void sendMessages(String message) throws IOException {
        //遍历所有在线的客户端
        for(Map.Entry<String, ssmWebSocket> sessionEntry : clients.entrySet()) {
            String erp = sessionEntry.getKey();
            ssmWebSocket webSocket = sessionEntry.getValue();
            Session session = webSocket.session; //获取当前客户端的session
            log.info("服务端给客户端[{}]发送消息{}", erp, message);
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("{}发送消息发生异常，异常原因{}", this.erp, message);
            }
        }
    }

    /**
     * 当收到客户端发送的消息时，会调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        log.info("服务端收到客户端[{}]发送的消息：{}", this.erp, message);
        // 模拟心跳机制：
        //    前端可以通过setInterval定时任务每个15秒钟调用一次reconnect函数
        //    reconnect会通过socket.readyState来判断这个websocket连接是否正常
        //    如果不正常就会触发定时连接，每15s钟重试一次，直到连接成功
        if(message.equals("ping")) { //如果收到客户端发送的ping
            this.sendMessage("pong", session); //服务端返回一个pong，来不断重连，不会超时断连
        }
    }
}
